Exploring the Effects of ULEZ 2023 Expansion

Youran Xu

Gathering Input Data

  • Openair R package 1
    • Air Quality in England
    • Automatic Urban and Rural Network
  • Supplementary Data from London-wide ULEZ Six Month Report 2024

Sites used in ULEZ report

  • 176 sites for NO2

  • 69 sites for PM2.5

198 sites from ULEZ report due to overlaps

## List of NO2 Monitoring Sites ####
sites_NO2 <- read_excel("Supplementary Data Sheet.xlsx", 
                        sheet = "List of NO2 Monitoring Sites")
sites_NO2 <- as.data.frame(sites_NO2)
## List of PM2.5 Monitoring Sites ####
sites_PM2.5 <- read_excel("Supplementary Data Sheet.xlsx", 
                        sheet = "List of PM2.5 Monitoring Sites")
sites_PM2.5 <- as.data.frame(sites_PM2.5)
## merge ####
sites_ulez <- full_join(sites_NO2, sites_PM2.5) ## remove overlaps between pollutants
sites_ulez <- sites_ulez %>% distinct() ## double check no duplicates
nrow(sites_ulez)
[1] 198
rm(sites_NO2, sites_PM2.5)
sites_ulez <- sites_ulez %>% 
  mutate(Name = ifelse(Name == "Beech Street NOX", "Beech Street NOx", Name))

Cross-referencing ULEZ sites and openair emission data

## AURN 
aurn <- importAURN(
  site = sites_ulez$`Site Code`,
  year = 2014 : 2024,
  data_type = "monthly",
  pollutant = c("nox", "no2", "no", "pm2.5", "pm10")
)
## AQE
aqe <- importAQE(
  site = sites_ulez$`Site Code`,
  year = 2014:2024,
  data_type = "monthly",
  pollutant = c("nox", "no2", "no", "pm2.5", "pm10")
)
## join
data <- aurn %>% 
  select(-3) %>% 
  full_join(aqe)
rm(aqe, aurn)
# 95 sites availible
data %>% 
  group_by(code) %>% 
  tally() %>% 
  nrow()
[1] 95
  • A total of 95 unique sites will be used in this analysis.

Transforming data set

## prepare variables for pivot
data <- data %>% 
  rename(no_concentration = no,
         no2_concentration = no2,
         nox_concentration = nox,
         pm2.5_concentration = pm2.5,
         pm10_concentration = pm10)
  
## pivot into long format
data <- data %>% 
  pivot_longer(
    cols = no_concentration:pm2.5_capture,
    names_to = c("pollutant", ".value"),
    names_sep = "_",
    values_drop_na = TRUE
  )

## only leave observations with a capture rate higher than 0.8
data <- data %>% 
  mutate(pollutant = factor(pollutant)) %>% 
  filter(capture >=0.8)
## add information about site type and area from ULEZ data set
data <- data %>% 
  left_join(sites_ulez, c("code" = "Site Code", "site" = "Name"))
# check for NAs
data %>%
filter(is.na(Area) | is.na(`Site Type`))

## add geographic coordinates from meta data
data <- data %>% 
  left_join(sites, by = join_by(code))
# check for NAs
data %>%
  filter(is.na(latitude))


## factorise variables
data <- data %>% 
  mutate(`Site Type` = factor(`Site Type`)) %>% 
  mutate(Area = factor(Area))

# adjust the order of area
table(data$Area)
data$Area <- factor(data$Area, 
                    levels = c("Central London",
                               "Inner London",
                               "Outer London",
                               "5 km from GLA Boundary",
                               "5-40 km from GLA Boundary"))
levels(data$Area)
data <- data %>% 
  mutate(source = factor(source))

saveRDS(data, file = "data.rds")

Data Overview

  • 2 sources (AQE and AURN)
  • from Jan 2014 to October 2024
  • 95 monitoring sites
  • Data capture rate above 0.8
  • pollutants: NO, NO2, NOX, PM10, PM2.5
  • 18828 Roadside and 13118 Urban Background sites

Network Visualisation

::: aside Cited from Inner London Ultra Low Emission Zone – One Year Report 2023

:::

Observations of monthly average concentration for each pollutant:

Pollutant NO. Observations
NO 7702
NO2 7698
NOX 7698
PM10 5516
PM2.5 3332

Calculating monthly % change for NOX

mean_mon_pre <- data %>% 
  filter(pollutant == "nox") %>% 
  filter(date >= as.Date("2022-07-01") & date <= as.Date("2023-09-01")) %>% 
  group_by(date) %>% 
  summarise(mean_monthly = mean(concentration)) %>% 
  mutate(pct_change = (mean_monthly - lag(mean_monthly))/lag(mean_monthly) * 100)
  
mean_mon_pre %>% 
  filter(!is.na(pct_change)) %>%
  summarise(mean = mean(pct_change))

mean_mon_aft <- data %>% 
  filter(pollutant == "nox") %>% 
  filter(date >= as.Date("2023-09-01")) %>% 
  group_by(date) %>% 
  summarise(mean_monthly = mean(concentration)) %>% 
  mutate(pct_change = (mean_monthly - lag(mean_monthly))/lag(mean_monthly) * 100) 

mean_mon_aft %>% 
  filter(!is.na(pct_change)) %>%
  summarise(mean = mean(pct_change))

pct_change <- full_join(mean_mon_pre, mean_mon_aft)  
Date Average Monthly Concentration Monthly % Change
Jul 22 34.94 /
Aug 22 37.52 7.37%
Aug 24 23.97 -17.34%
Sept 24 28.35 18.26%
Oct 24 31.01 9.40%
Before ULEZ 2023 After ULEZ 2023
4.16% -1.15%

Monthly percentage change for NOX before & after ULEZ expansion 2023

Results

# data <- readRDS("data.rds")
theme_set(theme_minimal())
# overview ####
## all pollutants 2014-2024 ####
data %>% 
   ggplot(aes(x = as.Date(date), y = concentration, colour = pollutant)) +
   geom_smooth() +
   scale_x_date(date_breaks = "1 year", labels = date_format("%Y")) +
   theme(axis.text.x = element_text(angle = 30, hjust = 1)) +
   xlab("") +
   ylab("Monthly Average Concentration [μgm-3]") +
   ggtitle("Trends in monthly average air pollution concentrations in London", "From January 2014 to October 2024") +
   annotate("rect", xmin = as.Date("2023-09-01"), 
            xmax = as.Date("2024-10-01"), 
            ymin = -1, ymax = 150,
           alpha = .1,fill = "green") +
   annotate("text", x = as.Date("2024-04-01"), y = 130, label = "ULEZ", alpha = 0.8) +
   annotate("text", x = as.Date("2024-04-01"), y = 123, label = "Expansion", alpha = 0.8)

Let us zoom in and have a look:

## all pollutants 2022-2024 ####

data %>% 
   filter(date >= as.Date("2022-10-01")) %>% 
   ggplot(aes(x = as.Date(date), y = concentration, colour = pollutant)) +
   geom_smooth() +
   scale_x_date(date_breaks = "3 month", labels = date_format("%Y %b")) +
   theme(axis.text.x = element_text(angle = 30, hjust = 1)) +
   xlab("") +
   ylab("Monthly Average Concentration [μgm-3]") +
   ggtitle("Trends in monthly average air pollution concentrations in London", "From October 2022 to October 2024") +
   annotate("rect", xmin = as.Date("2023-09-01"), 
            xmax = as.Date("2024-10-01"), 
            ymin = -1, ymax = 82,
           alpha = .1,fill = "green") +
   annotate("text", x = as.Date("2024-04-01"), y = 60, label = "ULEZ", alpha = 0.8) +
   annotate("text", x = as.Date("2024-04-01"), y = 55, label = "Expansion", alpha = 0.8)

Focus on NO2 after ULEZ expansion 2023…

# By Area and site type ####
## NO2 ####
data %>% 
  filter(date >= as.Date("2022-07-01")) %>% 
  filter(pollutant == c("no2")) %>% 
  ggplot(aes(x = as.Date(date), y = concentration, linetype = `Site Type`)) +
  geom_smooth() +
  facet_wrap(~ Area) +
  scale_x_date(date_breaks = "8 months", labels = date_format("%y %b")) +
   theme(axis.text.x = element_text(angle = 30, hjust = 1)) +
   xlab("") +
   ylab("Monthly Average NO2 Concentration [μgm-3]") +
   ggtitle("Trends in monthly average NO2 concentrations in London", "From July 2022 to October 2024") +
  annotate("rect", xmin = as.Date("2023-09-01"), 
            xmax = as.Date("2024-10-01"), 
            ymin = -1, ymax = 50,
           alpha = .1,fill = "green") +
  geom_hline(aes(yintercept = 40, linetype = "dashed")) +
  scale_linetype(labels = c("Roadside", "Urban Background", "Limit Value"))

PM2.5 after ULEZ expansion 2023…

data %>% 
  filter(date >= as.Date("2022-07-01")) %>% 
  filter(pollutant == c("pm2.5")) %>% 
  ggplot(aes(x = as.Date(date), y = concentration, linetype = `Site Type`)) +
  geom_smooth() +
  facet_wrap(~ Area) +
  scale_x_date(date_breaks = "8 months", labels = date_format("%y %b")) +
   theme(axis.text.x = element_text(angle = 30, hjust = 1)) +
   xlab("") +
   ylab("Monthly Average PM2.5 Concentration [μgm-3]") +
   ggtitle("Trends in monthly average PM2.5 concentrations in London", "From July 2022 to October 2024") +
  annotate("rect", xmin = as.Date("2023-09-01"), 
            xmax = as.Date("2024-10-01"), 
            ymin = -1, ymax = 20,
           alpha = .1,fill = "green") +
  geom_hline(aes(yintercept = 12, linetype = "dashed")) +
  scale_linetype(labels = c("Roadside", "Urban Background", "Environmental Improvement Plan"))

Conclusion

  • ULEZ 2023 expansion has been effective for its purpose.

  • Most effective for NO2 reduction in central London, outer London, and areas 5km from GLA boundary

  • Effective for PM2.5 reduction in central London

  • Effective for both roadside and urban background air quality

  • Results correspond with ULEZ six-month report from July.

Recommendations

  • The implementation of ULEZ zones could be promoted and generalised to other metropolitans.

  • Similar approach could be adopted for electric cars in the future.

  • Additional policies to improve urban air quality.

  • Meteorological data could be used to aid analysis in future reports.

Discussion

  • Technical:

    • Limited sample size (95/198)

    • Site name variations

    • openair package

  • Methodological:

    • Cross-comparison and chronological comparison

    • Use of control group (estimation) and treatment groups could improve validity, as % change shows inconsistant results.

    • Data smoothing reduces outliers from impacts of weather and seasonality, while losing data integrity and causing distortions.

  • Fit for trend-spotting but not impact analysis

Time for some Q&A…